/*
 * To change this template, choose Tools | Templates and open the template in
 * the editor.
 */
package terrain;
import java.util.ArrayList;
import com.jme3.math.Vector3f;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.PrecisionModel;

/**
 * Terrain surface class which provides flood fill functionality. Initially Found 
 * on http://www.javagaming.org forum and adapted for generating surfaces on terrains in jme3.
 * 
 * @author kingaschi (Christph Aschwanden - king@kingx.com)
 * @author moogie (Javagaming Forum)
 * @author tom  (Javagaming Forum)
 * @author makeshift (jMonkeyEngine.org)
 * @since April 26, 2005
 */
public final class TerrainSurfaceFactory {

  /** 
   * Line info class for linear non-recursive fill.
   * 
   * @author makeshift
   * @since Oktober 9, 2011
   */
  class LineInfo {
    
    /** The left position. */
    int left;
    /** The right position. */
    int right;
    /** The y position. */
    int y;  
    
    /**
     * Sets the line info.
     * 
     * @param left  Previous left position.
     * @param right  Previous right position.
     * @param y  Y position.
     */
    void setInfo(int left, int right, int y) {
      this.left = left;
      this.right = right;
      this.y = y;
    }
  }


  /** The array used for fast flood fill. Instantiated only once to improve performance. */
  private ArrayList<LineInfo> linearNRTodo = new ArrayList<LineInfo>();
  /** The index into linear non-recursive fill. */
  private int index;
  

  /** The raw heightMap data to fill. */
  private float[] heightMap;   
  /** The heightMap data mask that allows several surfaces to be generated. */
  private float[] heightMapMask;
  /** The start x position for the fill. */
  private int startX;
  /** The start y position for the fill. */
  private int startY;
  /** The info about surface creation */ 
  TerrainSurfaceInfo terrainSurfaceInfo;
  
  private static final GeometryFactory GEOMETRY_FACTORY = new GeometryFactory(new PrecisionModel());

  /**
   * Base constructor for Terrain Surface factory, for when TerrainSurfaceInfo will be supplied later.
   * 
   */
  public TerrainSurfaceFactory() {
  }
  
  /**
   * Constructor for Terrain Surface factory, requires a TerrainSurfaceInfo instance.
   * 
   * @param surfaceCreationConfig    The config class for surface creation.
   */
  public TerrainSurfaceFactory(TerrainSurfaceInfo terrainSurfaceInfo) {
  setSurfaceInfo(terrainSurfaceInfo);
  }

  public final void setSurfaceInfo(TerrainSurfaceInfo terrainSurfaceInfo){
  this.terrainSurfaceInfo = terrainSurfaceInfo;
  this.heightMap = terrainSurfaceInfo.terrain.getHeightMap();
  }
  public TerrainSurfaceInfo getSurfaceInfo(){
  return terrainSurfaceInfo;
  }

   
  /** 
   * Creates a new TerrainSurface starting from a given location.
   * 
   * @param location    The starting location for building a terrainSurface. Commonly location of a pond shore.
   */
  public TerrainSurface createSurface(String name,Vector3f startLocation) {
  return (terrainSurfaceInfo == null) ? null : generateSurface(startLocation,new TerrainSurface(name,terrainSurfaceInfo));
  }
  
  /** 
   * Rebuilds a given TerrainSurface.
   * 
   * @param location    The starting location for building a terrainSurface. Commonly location of a pond shore.
   * @param terrainSurface The supplied terrainSurface to be replaced.
    */
  public TerrainSurface replaceSurface(Vector3f location, TerrainSurface terrainSurface){
  terrainSurface.surfaceLines.clear();
  return (terrainSurfaceInfo == null) ? null : generateSurface(location,terrainSurface);
  }
  
  /** 
   * Generates the actual TerrainSurface data.
   * Uses linescan floodfilling to find the edges of the terrain surface.
   * 
   * @param location    The starting location for building a terrainSurface. Commonly the location of a pond shore.
   * @param terrainSurface The supplied terrainSurface to store the data in.
    */  
  private TerrainSurface generateSurface(Vector3f location, TerrainSurface terrainSurface) {
    this.heightMapMask = this.heightMap.clone();
      
    this.startX = Math.round(location.x/terrainSurfaceInfo.terrain.getLocalScale().x)+terrainSurfaceInfo.terrainHalfSize;
    this.startY = Math.round(location.z/terrainSurfaceInfo.terrain.getLocalScale().z)+terrainSurfaceInfo.terrainHalfSize;
    // The height of the terrain is stored in the TerrainSurface from the start.
    terrainSurface.heightMapHeight = heightMap[startX+startY*terrainSurfaceInfo.terrainSize];    

    terrainSurface.resetVolumeCount();
    // init stack
    linearNRTodo.clear();
    index = 0;
        
    floodFill(startX, startY,terrainSurface); 

    // loop through todo list
    while (index < linearNRTodo.size()) {
      // get loop data
      LineInfo lineInfo = linearNRTodo.get(index);
      index++;
      int y = lineInfo.y;
      int left = lineInfo.left;
      int right = lineInfo.right;
      
      // check top
      if (y > 0) {
        int yOff = (y - terrainSurfaceInfo.lod) * terrainSurfaceInfo.terrainSize;
        int x = left;
        while (x <= right) {
            
            
          int heightMapRef = yOff + x;
          float heightMapHeighter = (heightMapRef>=0&&heightMapRef<=terrainSurfaceInfo.terrainSquaredSize) ? heightMapMask[heightMapRef] : terrainSurface.heightMapHeight;            
          if (heightMapHeighter < terrainSurface.heightMapHeight) {
            x = floodFill(x, y - terrainSurfaceInfo.lod,terrainSurface);
          }
          x+=terrainSurfaceInfo.lod;
        }
      }

      // check bottom
      if (y < (terrainSurfaceInfo.terrainSize - 1)) {
        int yOff = (y + terrainSurfaceInfo.lod) * terrainSurfaceInfo.terrainSize;
        int x = left;
        while (x <= right) {
          int heightMapRef = yOff + x;
          float heightMapHeighter = (heightMapRef>=0&&heightMapRef<terrainSurfaceInfo.terrainSquaredSize) ? heightMapMask[heightMapRef] : terrainSurface.heightMapHeight;            
          if (heightMapHeighter < terrainSurface.heightMapHeight) {
            x = floodFill(x, y + terrainSurfaceInfo.lod,terrainSurface);
          }
          x+=terrainSurfaceInfo.lod;
        }
      }
    }
    return terrainSurface;
  } 
  
  /** 
   * Fills the line at (x, y). And adds to the stack. 
   * 
   * @param x  The x-coordinate of the start position.
   * @param y  The y-coordinate of the start position.
   * @return Right.
   */ 
  private int floodFill(int x, int y,TerrainSurface terrainSurface) {      
    int yOff = y * terrainSurfaceInfo.terrainSize;
    
    // fill left of (x,y) until border or edge of image
    int left = x;
    do {
        // If findVolume flag is set, find the volume.       
        if(terrainSurfaceInfo.findVolume){
        terrainSurface.addVolume((terrainSurface.heightMapHeight-heightMapMask[yOff + left])*terrainSurfaceInfo.lodSquared);
        }        
      heightMapMask[yOff + left] = terrainSurface.heightMapHeight;      
      left-=terrainSurfaceInfo.lod;
    } 
    while ((left >= 0) && (heightMapMask[yOff + left] < terrainSurface.heightMapHeight));
    left+=terrainSurfaceInfo.lod;
    
    // fill right of (x, y) until border or edge of image
    int right = x;
    do {
        // If findVolume flag is set, find the volume.
        if(terrainSurfaceInfo.findVolume){
        terrainSurface.addVolume((terrainSurface.heightMapHeight-heightMapMask[yOff + right])*terrainSurfaceInfo.lodSquared);
        }
      heightMapMask[yOff + right] = terrainSurface.heightMapHeight;
      right+=terrainSurfaceInfo.lod;
    } 
    while ((right < terrainSurfaceInfo.terrainSize) && (heightMapMask[yOff + right] < terrainSurface.heightMapHeight));
    right-=terrainSurfaceInfo.lod;

    // add to stack
    if (index == 0) {
      LineInfo lineInfo = new LineInfo();
      lineInfo.setInfo(left, right, y);
      linearNRTodo.add(lineInfo);
    }
    else {
      index--;
      linearNRTodo.get(index).setInfo(left, right, y);
    }
    // If createShape flag is set, create shape.
    if(terrainSurfaceInfo.createShape){
    Coordinate[] shellPoints = new Coordinate[]{
    new Coordinate(left-terrainSurfaceInfo.terrainHalfSize,y-terrainSurfaceInfo.terrainHalfSize),
    new Coordinate(right-terrainSurfaceInfo.terrainHalfSize,y-terrainSurfaceInfo.terrainHalfSize),
    };
    // These are the lines used when creating the terrainsurface mesh with shape3d.
    terrainSurface.addLine(GEOMETRY_FACTORY.createLineString(shellPoints));
    }
    return right;
  }
}